--[[
Name: BrownieHelper
Developed by: Sonora (The Dragonflight)
Website: http://sonora.kirintor.googlepages.com/contents
SVN: http://code.google.com/p/browniehelper/source/browse
License: GNU General Public License v3
]]

--[[
Ace2 implementation of the BrownieHelper application

The application ties the bidding system to the UI and other game facilities.

~Custom Events~
BrownieHelper_MasterLooterChanged -> calls handler(newMasterLooter,oldMasterLooter)
BrownieHelper_PointsChanged -> calls handler()
BrownieHelper_EventsChanged -> calls handler()
BrownieHelper_PlayerJoinedGroup -> calls handler(characterName, characterInfo)
BrownieHelper_PlayerLeftGroup -> calls handler(characterName, characterInfo)
BrownieHelper_PlayerDbChanged -> calls handler(playerName)
BrownieHelper_ModeChanged -> calls handler(mode) (fires if the mode has changed, or the master looter has changed for client mode)
BrownieHelper_LootChanged -> calls handler()
BrownieHelper_AuctionStateChanged -> calls handler(state, itemInfo, bidSetup, bids) whenever the auction state changes (see auctionhandler.lua for a list of states)
BrownieHelper_BidsChanged -> calls handler(bids)
BrownieHelper_BiddingOpened -> calls handler(playerBidSetup); playerBidSetup is false if the bidding is closed
BrownieHelper_WinnerChanged -> calls handler(winningBid)

Note: the AceEvent call obj:RegisterEvent(name, handler) will only allow a
single handler to be registered for each event name.  Our internal...

BrownieHelper:registerEvent(name, handler)

..will let multiple handlers register across the mod for each event.  It will
also automatically register itself to be the recipient of the external dispatch.

Note: we hold event dispatches until the player is out of combat -- no need to
compete for processing and comm resources.

~Inter-player communication (called "commands")~
--Single round commands
HasBrownieHelper -> called by every player running brownie helper as part of building the roster; has no args.
GetPointsDatabase -> called by one player to get the points database from another
GetPermissions -> called by each member whenever they see a new master looter
GetAuctionDetails -> call and response between client and server to get player-customized bid setup information
SubmitBid -> called on the master looter by members when they use the addon to place a bid

--Situational dispatch commands
GetPointUpdates -> called by members to get point updates from the master looter when some windows are open; currently updates are only requested client side when the ViewPoints window is open
GetEventUpdates -> called by members to get event update from the master looter when some windows are open; currently updates are only requested client side when the ViewEvents window is open

--Always up dispatch commands
GetLootUpdates -> dispatched whenever the master looter "Announces" the items comming up for bidding; when in client mode we're always registed for this command
GetBiddingOpen -> dispatched to members when a new round of bidding opens
GetPermissionsChanged -> dispatched to members when the ML changes the permissions state

--Commands for auditors
GetRunningAuctionUpdates -> dispatched to any authorized auditors when the master looter opens the auction wizard window, updates when the bidding state changes
GetBidUpdates -> dispatched to authorized auditors as the bidding progresses
GetWinnerUpdates -> dispatched to authorized auditors when a winner is selected or deselected

]]--

--Version tracking
local __version__ = "1.5"
BROWNIEHELPER_VERSION = __version__ -- Global version identifier for the entire package

--Constants to track the mode in which the mod is operating:  MASTERLOOTER, AUDITOR, CLIENT
BROWNIEHELPER_MASTERLOOTER = 3      --we are the master looter, or not in a group
BROWNIEHELPER_AUDITOR = 2           --the master looter has us marked as an auditor
BROWNIEHELPER_CLIENT = 1            --we are in a group with a master looter
BROWNIEHELPER_QUIESCENT = 0         --we are in a group but there is no master looter, or they don't have brownie helper

BROWNIEHELPER_NOMASTERLOOTER = -1

--Application class instantiation
BrownieHelper = AceLibrary("AceAddon-2.0"):new("AceConsole-2.0", "AceEvent-2.0", "AceDB-2.0", "AceModuleCore-2.0", "FuBarPlugin-2.0", "AceComm-2.0")

--[[
Application DB registration and defaults.

If the addon is used in conjunction with the external BrownieHelper script then
many of these values will be initialized/overwritten direct in the saved
variables file when the script is run.
]]--
BrownieHelper:RegisterDB("BrownieHelperDB2", "BrownieHelperDBPC2") --we use ver 2 of the BrownieHelper database (not backwards compatible with ver 1)

--Our defaults just setup some bogus data for testing purposes.  Run the helper
--script to write real/clean values to the database
BrownieHelper:RegisterDefaults("realm", {
    --[[
    Database of the runs that have been recorded by the mod
    
    Events that are auto generated are:
    Adjusted points
    Player joined
    Player left
    Auction won
    
    runDb = {
        ["09/19/07 Gruul's Lair"] = {
                ["note"] = "sample note",
                ["events"] = {
                        ["02:37 - Added player: Sonor"] = {
                                ["event"] = "Adjusted points",
                                ["player"] = "Sonor",
                                ["gametime"] = "02:37",
                                ["uploaded"] = false,
                                ["description"] = "Points adjusted for player Sonor.",
                        },
                        ["21:22 - Auction won: Sonor"] = {
                                ['event'] = 'Auction won',
                                ['player'] = "Sonor",
                                ['character'] = 'SonorAlt1',
                                ['gametime'] = '21:22',
                                ['itemlink'] = "|cffa335ee|Hitem:30249:0:0:0:0:0:0:693688678|hPauldrons of the Vanquished Defender|h|r",
                                ['itemname'] = "Pauldrons of the Vanquished Defender",
                                ["description"] = "Sonor won Pauldrons of the Vanquished Defender in a First phase auction",
                                ["bids"] = "Sonor 52",
                                ["pointcost"] = "32",
                                ["biddingsystem"] = "First phase",
                                ["uploaded"] = false,
                        },
                },
                ["point_pool"] = "Gruul's Lair",
        },
    },
    ]]
    runDb = {},
    
    --[[
    Database of the points each player has banked for each point pool
    
    Note: this structure is written by the BrownieHelper utility script, if you
    are using it in conjunction with the addon.
    ]]
    pointsDb = {
        ["Magtheridon's Lair"] = {
        },
        ["Serpent Shrine Cavern"] = {
        },
        ["The Eye"] = {
        },
        ["Gruul's Lair"] = {
        },
        ["Hyjal Summit"] = {
        },
        ["Sunwell Plateau"] = {
        },
        ["Black Temple"] = {
        },
    },
    
    --[[
    Database of information about players

    playerDb = {
        ["Sonor"] = {
            ["aliases"] = {'SonorAlt1', 'SonorAlt2'},
            ["class"] = "Hunter",
        },
    },
    ]]
    playerDb = {},
    
    --[[
    Default minimum bids indexed by point pool name
    
    Note: this structure is written by the BrownieHelper utility script, if you
    are using it in conjunction with the addon
    ]]
    MinimumBids = {
        ["Serpent Shrine Cavern"] = "32",
        ["Hyjal Summit"] = "32",
        ["Sunwell Plateau"] = "32",
        ["Black Temple"] = "32",
        ["Magtheridon's Lair"] = "16",
        ["The Eye"] = "32",
        ["Gruul's Lair"] = "24",
    },
    
    --[[
    These are manually added reward events that record time and player roster
    
    Note: this structure is written by the BrownieHelper utility script, if you
    are using it in conjunction with the addon
    ]]
    Events = {
        ["Serpent Shrine Cavern"] = {
            "Raid start time", -- [1]
            "Time adjustment", -- [2]
            "Hydross the Unstable", -- [3]
            "The Lurker Below", -- [4]
            "Leotheras the Blind", -- [5]
            "Fathom-Lord Karathress", -- [6]
            "Morogrim Tidewalker", -- [7]
        },
        ["Sunwell Plateau"] = {
            "Raid start time", -- [1]
            "Time adjustment", -- [2]
            "Kalecgos", -- [3]
            "Brutallus", -- [4]
            "Felmyst", -- [5]
            "Eredar Twins", -- [6]
            "M'uru", -- [7]
            "Kil'jaeden", -- [8]
        },
        ["Black Temple"] = {
            "Raid start time", -- [1]
            "Time adjustment", -- [2]
            "High Warlord Naj'entus", -- [3]
            "Supremus", -- [4]
            "Shade of Akama", -- [5]
            "Teron Gorefiend", -- [6]
            "Gurtogg Bloodboil", -- [7]
            "Reliquary of Souls", -- [8]
            "Mother Shahraz", -- [9]
            "The Illidari Council", -- [10]
            "Illidan Stormrage", -- [11]
        },
        ["Magtheridon's Lair"] = {
            "Raid start time", -- [1]
            "Time adjustment", -- [2]
            "Magtheridon", -- [3]
        },
        ["The Eye"] = {
            "Raid start time", -- [1]
            "Time adjustment", -- [2]
            "Al'ar", -- [3]
            "Void Reaver", -- [4]
            "High Astromancer Solarian", -- [5]
            "Kael'thas Sunstrider", -- [6]
        },
        ["Gruul's Lair"] = {
            "Raid start time", -- [1]
            "Time adjustment", -- [2]
            "Gruul the Dragonkiller", -- [3]
            "High King Maulgar", -- [4]
        },
        ["Hyjal Summit"] = {
            "Raid start time", -- [1]
            "Time adjustment", -- [2]
            "Anetheron", -- [3]
            "Archimonde", -- [4]
            "Azgalor", -- [5]
            "Kaz'rogal", -- [6]
            "Rage Winterchill", -- [7]
        },
    },
    
    --[[
    EventRewards = {
        ["Gruul the Dragonkiller"] = "4",
        ["Raid start time"] = "4",
        ["High King Maulgar"] = "4",
        ["Time adjustment"] = "4",
    },]]
    EventRewards = {},
    
    --[[
    Map of names of different types of loot to plain item names.  In the bbp
    bidding system, unnamed items default to "Boss loot"
    
    Note: this structure is written by the BrownieHelper utility script, if you
    are using it in conjunction with the addon
    ]]
    LootTypes = {
        ["BOP reagents"] = {
            "Nether vortex", -- [1]
        },
        ["BOP item recipes"] = {
            "Plans: Red Havoc Boots", -- [1]
            "Plans: Boots of the Protector", -- [2]
            "Pattern: Hurricane Boots", -- [3]
            "Pattern: Boots of Natural Grace", -- [4]
            "Pattern: Boots of the Crimson Hawk", -- [5]
            "Pattern: Boots of Utter Darkness", -- [6]
            "Pattern: Belt of Deep Shadow", -- [7]
            "Pattern: Belt of Natural Power", -- [8]
            "Pattern: Boots of Blasting", -- [9]
        },
        ["BOE item recipes"] = {
            "Plans: Belt of the Guardian", -- [1]
            "Plans: Red Belt of Battle", -- [2]
            "Pattern: Belt of Deep Shadow", -- [3]
            "Pattern: Belt of Natural Power", -- [4]
            "Pattern: Belt of the Black Eagle", -- [5]
            "Pattern: Monsoon Belt", -- [6]
            "Pattern: Belt of Blasting", -- [7]
            "Pattern: Belt of the Long Road", -- [8]
        },
    },
    
    --[[
    Mapping of different permissions -- associates permission name with a list
    of players.
    
    Note: this structure is written by the BrownieHElper utlity script, if you
    are using it in conjunction with the addon

    Permissions = {
        ["auditors"] = {"Sonor",},
    },
    ]]
    Permissions = {
        ["auditors"] = {},
    },
    
    --[[
    A collection of miscellaneous options of local relevance
    ]]
    Options = {
        ['CloseAuctionDelay'] = "10",
        ['BiddingInstructions'] = "You can whisper me with 'bid 5' to place a bid (but replace 5 with your actual bid). Whisper me with 'help?' for more info.",
        ['ShowBidWindow'] = true,
        ['ShowItemWindow'] = true,
        ['ShowAuctionWindow'] = true,
        ['DefaultRaidStartTime'] = "20:00",
        ['TimeAdjustmentPeriod'] = "60",
        ['AutoRecordTimeAdjustment'] = true,
        ['AutoRecordRaidStart'] = true,
        ['AutoRecordBossKills'] = true,
        ['DefaultBiddingSequence'] = {'Simple Auction'},
    },
    
    --[[
    A record of the last time the local saved variables points database was
    updated (in UTC)

    lastUpdated = "09/09/07 21:00",
    ]]
    lastExternalUpdate = "",
    lastInternalUpdate = "",
    
})

--[[
Ace2 application event overrides

]]
function BrownieHelper:OnInitialize()
    -- Called when the addon is loaded

    --Register our communication commands here, and the event handlers that will watch them; see top of the module for a description of each
    self:registerCommand("HasBrownieHelper")
    self:registerCommand("GetPermissions")
    self:registerCommand("UpdateRoster")
    
    self:registerDispatchingCommand("GetPointUpdates")
    self:registerEvent("BrownieHelper_PointsChanged", "sendPointUpdates")
    
    self:registerDispatchingCommand("GetEventUpdates")
    self:registerEvent("BrownieHelper_EventsChanged", "sendEventUpdates")
    
    self:registerDispatchingCommand("GetLootUpdates")
    
    self:registerDispatchingCommand("GetBiddingOpen")
    self:registerEvent("BrownieHelper_AuctionStateChanged",
        function(state, itemInfo, bidSetup)
            if state==BROWNIEHELPER_AUCTIONRUNNING then self:sendBiddingOpen(bidSetup)
            elseif state==BROWNIEHELPER_POSTAUCTION then self:sendBiddingOpen(false)
            end
        end
    )
    
    self:registerCommand("GetAuctionDetails")
    self:registerCommand("SubmitBid")
    
    --Commands that are just for authorized auditors (authorization is always local)
    self:registerDispatchingCommand("GetPermissionsChanged")
    self:registerEvent("BrownieHelper_PlayerDbChanged", "sendGetPermissionsChanged")
    
    self:registerDispatchingCommand("GetRunningAuctionUpdates", nil, function(client) return self:checkPermission("auditors",client) end) --we pass in a clientCheck function to make sure we only respond to auditors.
    self:registerEvent("BrownieHelper_AuctionStateChanged", "sendRunningAuctionUpdates")
    self:registerEvent("BrownieHelper_AuctionStateChanged", function(state) self.auctionState=state end)    
    
    self:registerDispatchingCommand("GetBidUpdates", nil, function(client) return self:checkPermission("auditors",client) end) --we pass in a clientCheck function to make sure we only respond to auditors.
    self:registerEvent("BrownieHelper_BidsChanged", "sendBidUpdates")
    
    self:registerDispatchingCommand("GetWinnerUpdates", nil, function(client) return self:checkPermission("auditors",client) end)
    self:registerEvent("BrownieHelper_WinnerChanged", "sendWinnerUpdates")
    
    --Comm setup:  make sure to register any commands we know about BEFORE calling this (this is when memoization happens)
    self:setupCommunication()
    
    --Setup speakeasy
    self:setupWhisperCommands()
    
    --Setup a couple of local calls that handle transitioning the various things we need to when we switch from "client" to "server" modes
    self:registerEvent("BrownieHelper_ModeChanged",
        function(mode)
            if mode>=BROWNIEHELPER_MASTERLOOTER then self:OnServerMode()
            elseif mode>=BROWNIEHELPER_AUDITOR then self:OnAuditorMode()
            elseif mode>=BROWNIEHELPER_CLIENT then self:OnClientMode()
            else self:OnQuiescentMode() end
        end
    )
    
    --Setup a callback to update our mode whenever the ML changes in the group (can happen after reprocessing the roster)
    self:registerEvent("BrownieHelper_MasterLooterChanged", "updateMode")
    
    --Handle recording events etc when players leave and join the group
    self:registerEvent("BrownieHelper_PlayerLeftGroup", function(characterName, characterInfo) self:addEvent("Left group", {player=characterInfo.playername, character=characterName, description=characterName.." left the group."}) end)
    self:registerEvent("BrownieHelper_PlayerJoinedGroup", function(characterName, characterInfo) self:addEvent("Joined group", {player=characterInfo.playername, character=characterName, description=characterName.." joined the group."}) end)
    
    --Make sure we never lack a points entry for the current point db for the players on the roster
    self:registerEvent("BrownieHelper_PlayerJoinedGroup", function(characterName, characterInfo) self:checkForPlayersWithoutPoints(characterName) end)
    self:registerEvent("BrownieHelper_ModeChanged", function() self:checkForPlayersWithoutPoints() end)
    
    --Record internal point updates
    self:registerEvent("BrownieHelper_PointsChanged", function() self:getDb().lastInternalUpdate = getFormattedGameDateTime() end)
    
    --Register a chat watcher that will be active durring auctions to watch for rolls
    self:registerEvent("CHAT_MSG_SYSTEM", function(message, author, language, status) self:WatchRolls(message) end)
    
    --Setup a roster that we will manage; the internal roster gets mutated in place so its safe to store extra metadata on
    self.roster = {}
    self:processRosterUpdate()
    
    --Whenever the player database is changed, scan through the roster and update player names on the character entries
    self:registerEvent("BrownieHelper_PlayerDbChanged",
        function() --this is a horribly inefficient implementation, but it'll be called rarely.
            for characterName, characterInfo in pairs(self.roster) do
                characterInfo.playername = self:checkAliasesFor(characterName)
            end
        end
    )
    
    --Setup a list of loot items that can be auctioned off
    self.loot = {}
    
    --Build the main menu for the addon
    self.options = {
        handler=self,
        type = "group",
        
        --Main menu actions
        args = {
            --Run database
            runDatabase = {
                type = 'execute',
                name = 'Runs',
                desc = 'Opens a window where you can view and edit the current run.\n\nOnly available in master looter mode.',
                func = "viewRunDatabase",
                disabled = function() return self:getMode()<BROWNIEHELPER_MASTERLOOTER end,
                order = 100,
            },
            
            --Player database
            playerDatabase = {
                type = 'execute',
                name = 'Players',
                desc = 'Opens a window where you can view and edit the current player database.  You can add new players, remove players, and edit their raid aliases (alt characters which bid with main charaters point pools).\n\nOnly available in master looter mode.',
                func = "viewPlayerDatabase",
                disabled = function() return self:getMode()<BROWNIEHELPER_MASTERLOOTER end,
                order = 110,
            },
            
            --Points database
            pointsDatabase = {
                type = 'execute',
                name = 'Points',
                desc = 'Opens a window where you can view and edit the current points database.',
                func = "viewPointsDatabase",
                order = 120,
            },
            
            --Events database
            eventsDatabase = {
                type = 'execute',
                name = 'Events',
                desc = 'Opens a window where you can view and edit event associated with the current run.',
                func = "viewEventDatabase",
                order = 130,
            },
            
            h100 = {
                type = "header",
                order = 199,
            },
            
            --Start bidding
            startBidding = {
                type = 'execute',
                name = 'Start an auction',
                desc = 'Start the auction wizard.  Give out phat loot.\n\nOnly available in master looter mode.',
                func = "startAuctionWizard",
                disabled = function() return self:getMode()<BROWNIEHELPER_MASTERLOOTER end,
                order = 200,
            },
            
            --Quick event adder
            addEvent = {
                type = 'group',
                name = 'Record an event',
                desc = 'Opens a side menu with a list of the events that you can currently record in the run database.',
                args = {}, --need to set after ViewEventsFrame gets intialized
                disabled = function() return self:getMode()<BROWNIEHELPER_MASTERLOOTER end,
                order = 210,
            },
            
            h200 = {
                type = "header",
                order = 299,
            },
            
            --Update points
            updatePoints = {
                type = 'execute',
                name = 'Download database',
                desc = 'Download database information from another player running BrownieHelper.',
                func = 'viewDownloadPoints',
                order = 300,
            },
            
            --Update points
            configure = {
                type = 'execute',
                name = 'Configure',
                desc = 'Opens the AddOns section of the built-in configuration UI, where you will find BrownieHelpers configuration options.',
                func = 'showConfiguration',
                order = 310,
            },
            
            h300 = {
                type = "header",
                order = 399,
            },
            
            forceML = {
                type = 'toggle',
                name = "Master looter mode",
                desc = "Forces the mod into master looter mode.  This is ignored if there is an actual master looter and you're not it.",
                get = function()
                    return self.forceML
                end,
                set = function(val)
                    self.forceML = val
                    self:processRosterUpdate()
                end,
                order = 400,
            },
        
        },
    }
    
    --Basic Ace2-ish setup
    self.hideWithoutStandby = true
    self.hasIcon = true
    self.OnMenuRequest = self.options
    self.mainDewDrop = AceLibrary("Dewdrop-2.0")
    
    --Setup the chat commands
    self.chatOptions = {
        type='group',
        args = {
            menu = {
                type = 'execute',
                name = "menu",
                desc = "Shows the main BrownieHelper menu",
                func = function()
                    self.mainDewDrop:Open(UIParent, 'children', function() self.mainDewDrop:FeedAceOptionsTable(self.options) end)
                end,
                order = 1,
            },
            forceML = {
                type = 'toggle',
                name = "forceML",
                desc = "Forces the mod into master looter mode.  This is overridden if there is an actual master looter.",
                get = function()
                    return self.forceML
                end,
                set = function(val)
                    self.forceML = val
                    self:processRosterUpdate()
                end,
                order = 3,
            },
            startauction = {
                type = 'execute',
                name = "startauction",
                desc = "Launches the auction wizard",
                func = function()
                    self:startAuctionWizard()
                end,
                order = 2,
            },
            configure = {
                type = 'execute',
                name = 'configure',
                desc = 'Configure BrownieHelper',
                func = function()
                    self:showConfiguration()
                end,
                order=4,
            },
        },
    }
    
    self:RegisterChatCommand({"/browniehelper", "/bh"}, self.chatOptions)
    
    --Inject our configuration panels into the stock UI config
    if self.initializeConfigurationPanels then self:initializeConfigurationPanels() end
    
end

function BrownieHelper:OnEnable()
    -- Called when the addon is enabled
    
    --Event handler registration
    self:registerEvent("RAID_ROSTER_UPDATE", "OnRosterChanged")
    self:registerEvent("PARTY_MEMBERS_CHANGED", "OnRosterChanged")
    self:registerEvent("LOOT_OPENED", "OnLootOpened")
    self:registerEvent("LOOT_SLOT_CLEARED", "OnLootOpened")
    

    -- not completely sure I'm getting the advantage of filtering at all, if COMBAT_LOG_EVENT fires for the lowest common filtering demoninator among all addons globally.
    -- is their design really that dumb? or am I missing something?
    --CombatLogResetFilter() -- can't do this b/c it would munge other addons
    CombatLogAddFilter("PARTY_KILL", nil, nil) 
    self:registerEvent("COMBAT_LOG_EVENT", "checkForBossEvent")
    
    
    --Basic minimap button or plugin setup
    if self:IsIconShown() then
	self:SetIcon("Interface\\Icons\\INV_Misc_Food_73CinnamonRoll") --closest I could find to a brownie?
    else
	self:HideIcon()
    end
    
    --Initialize view frames
    ViewRunsFrame:Initialize(self)
    ViewPlayersFrame:Initialize(self)
    ViewPointsFrame:Initialize(self)
    ViewDatabaseSourcesFrame:Initialize(self)
    ViewEventsFrame:Initialize(self)
    ViewItemsFrame:Initialize(self)
    EditBiddingSystemFrame:Initialize(self)
    AuctionWizardFrame:Initialize(self)
    AddBidFrame:Initialize(self)
    
    --Map main menu dew tabs on to those managed by other windows
    self.options.args.addEvent.args = ViewEventsFrame.eventDewTab.args
    
    --Update our mode setting whenever the mod is loaded
    self:updateMode()
    
end

--[[
Custom event dispatch system which allows more than one handler to register for
reception of each event name

]]
function BrownieHelper:registerEvent(name, handler)
    --if this is the first time we've been called
    if self.eventRegistry==nil then self.eventRegistry = {} end -- {name : {handlers,}}
    
    --if this is the first time we've seen this event
    if self.eventRegistry[name] == nil then
        
        --Create a new registry entry with this handler as the first item
        self.eventRegistry[name] = {handler,}
        
        --Without warning just overwrite any set event handlers
        self:RegisterEvent(name, function(...) self:dispatchEvent(name, ...) end)
        
    else
        --Just append it to the set
        table.insert(self.eventRegistry[name], handler)
    end
    
end

--[[
version 1.1 removed out-of-combat queueing of event dispatch...

I feel like this function should be able to be writtin like:

function BrownieHelper:dispatchEvent(name,...)

    self:ScheduleLeaveCombatAction(function() self:__dispatchEvent(name,...))

end

But alas, Lua does not appear to like binding the varargs variable ... to a
closure. Hence the ugly and incredibly breakable:

function BrownieHelper:dispatchEvent(name, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10)
    self:ScheduleLeaveCombatAction(function() self:__dispatchEvent(name, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10) end)
end


function BrownieHelper:__dispatchEvent(name,...)
--]]

function BrownieHelper:dispatchEvent(name,...)
    --handles event dispatch to all registered clients
    --DEBUG: pp("event dispatch of "..name)
    for i,currentHandler in pairs(self.eventRegistry[name]) do
        if type(currentHandler) == "function" then currentHandler(...)
        elseif type(currentHandler) == "string" then self[currentHandler](self, ...)
        end
    end
    
end

--[[
Methods that handle the roster and local player state

]]
function BrownieHelper:getRoster()
    --roster holds: {characterName=characterInfo}
    return self.roster
end

local rosterUpdatePending = false
function BrownieHelper:OnRosterChanged()
    --We bin roster changes in periods of 5 seconds to avoid excessive reprocessing of the roster
    if not rosterUpdatePending then
        self:ScheduleEvent(function() self:processRosterUpdate(); rosterUpdatePending=false; end, 5)
        rosterUpdatePending=true
    end
end

function BrownieHelper:processRosterUpdate()
    --Triggers PlayerLeftGroup, PlayerJoinedGroup and MasterLooterChanged as appropriate
    
    --Create a local variable
    local newMasterLooter = BROWNIEHELPER_NOMASTERLOOTER
    
    --Query the master looter situation
    local lootmethod, masterlooterPartyID, masterlooterRaidID = GetLootMethod()
    
    --Fill out a list of names currently in the raid
    local newcharacters = {[UnitName("player")]="player",}
    if lootmethod=="master" and (masterlooterPartyID==0 or masterlooterRaidID==0) then newMasterLooter=UnitName("player") end
    
    --If there's no real master looter, and the forceML flag is on, then set us to be the ML
    if self.forceML and lootmethod~="master" then
        newMasterLooter = UnitName("player")
    end
        
    --If we're in a raid, collect the character names for everyone in the raid
    if GetNumRaidMembers() >0 then
        for idx = 1,GetNumRaidMembers() do
            local unitid = "raid"..tostring(idx)
            newcharacters[UnitName(unitid)] = unitid
            if lootmethod=="master" and masterlooterRaidID==idx then newMasterLooter=UnitName(unitid) end
        end
        
    --If we're in a party, collect the character names for everyone in the party
    elseif GetNumPartyMembers() >0 then
        for idx = 1,GetNumPartyMembers() do
            local unitid = "party"..tostring(idx)
            newcharacters[UnitName(unitid)] = unitid
            if lootmethod=="master" and masterlooterPartyID==idx then newMasterLooter=UnitName(unitid) end
        end
        
    end
    
    --Before we start doing updates, remember the old master looter
    local oldMasterLooter = self:getMasterLooter()
    self.currentMasterLooter = newMasterLooter
    
    --For each character we didn't find, remove them from the roster and trigger an event
    for characterName, characterInfo in pairs(self.roster) do
        if newcharacters[characterName]==nil then
            
            --Remove the player from the roster
            self.roster[characterName] = nil
            
            --Trigger an event to notify that this player has left the group
            self:TriggerEvent("BrownieHelper_PlayerLeftGroup", characterName, characterInfo)
            
        end
    end
    
    --For each new character that we've found, add them to the roster, and trigger an event
    for characterName,unitid in pairs(newcharacters) do
        if self.roster[characterName]==nil then
            
            --Create a new entry for them
            self.roster[characterName] = {
                class = UnitClass(unitid),
                masterlooter = (characterName==newMasterLooter),
                playername = self:checkAliasesFor(characterName),
                unitid = unitid, --unitid is not reliable... since its only cataloged the first time we see a new player.
            }
            
            --Trigger an event to notify that this player has joined the group
            self:TriggerEvent("BrownieHelper_PlayerJoinedGroup", characterName, self.roster[characterName])
            
        end
    end
    
    --If the effective master looter state has changed since we last checked, send a notification
    if newMasterLooter~=oldMasterLooter then self:TriggerEvent("BrownieHelper_MasterLooterChanged", newMasterLooter, oldMasterLooter) end
    
end

--[[
Methods to handle view component showing or updating

]]
function BrownieHelper:viewRunDatabase()
    ViewRunsFrame:Show()
end

function BrownieHelper:viewPlayerDatabase()
    --Opens the player database viewing and editing window.
    ViewPlayersFrame:Show()
end

function BrownieHelper:viewPointsDatabase()

    --if we're in masterlooter mode
    if self:getMode()>=BROWNIEHELPER_MASTERLOOTER then 
        
        --Only show the points database window if a run is currently selected
        if not self:getCurrentRun() then
            handler = function(response)
                if response then ViewRunsFrame:Show() end
            end
            yesnoDialog("You can only view the points once you have setup the current run.  Would you like to open the run window now?", handler)
        end
        
    end
    
    ViewPointsFrame:Show()

end

function BrownieHelper:viewEventDatabase()

    --if we're in masterlooter mode
    if self:getMode()>=BROWNIEHELPER_MASTERLOOTER then 
        
        --Only show the points database window if a run is currently selected
        if not self:getCurrentRun() then
            handler = function(response)
                if response then ViewRunsFrame:Show() end
            end
            yesnoDialog("You can only view events once you have setup the current run.  Would you like to open the run window now?", handler)
        end
        
    end

    ViewEventsFrame:Show()
    
end

function BrownieHelper:startAuctionWizard()
    --The first step in the auction process is selecting an item

    if self:getMode()>=BROWNIEHELPER_MASTERLOOTER then 
        
        --Only show the points database window if a run is currently selected
        if not self:getCurrentRun() then
            handler = function(response)
                if response then ViewRunsFrame:Show() end
            end
            yesnoDialog("You can only run an auction when there's a current run selected.  Would you like to open the run window to select one now?", handler)
        end
        
    end    

    ViewItemsFrame:Show()
end

function BrownieHelper:viewDownloadPoints()
    ViewDatabaseSourcesFrame:Show()
end

function BrownieHelper:showConfiguration()

    --This should show the frame
    InterfaceOptionsFrame:Show()
    
    --This is what happens when you click on the AddOns tab
    PanelTemplates_Tab_OnClick(InterfaceOptionsFrame)
    PanelTemplates_UpdateTabs(InterfaceOptionsFrame)
    InterfaceOptionsFrame_TabOnClick()

end

--[[
Methods that handle addon mode managment

]]
function BrownieHelper:updateMode(forceUpdate)
    --mode will be one of the constants defined above
    
    local oldmode = self.mode
    local newMasterLooter = self:getMasterLooter()
    if self.masterLooterOnLastUpdate == nil then self.masterLooterOnLastUpdate = BROWNIEHELPER_NOMASTERLOOTER end
    
    --If we are the master looter
    if self:playerIsMasterLooter() then self.mode = BROWNIEHELPER_MASTERLOOTER
    --If we are an auditor (we know the master looter has brownie helper de facto)
    elseif newMasterLooter~=BROWNIEHELPER_NOMASTERLOOTER and self:playerIsAuditor() then self.mode = BROWNIEHELPER_AUDITOR
    --If we are a member of a group with a master looter, and they have BrownieHelper
    elseif newMasterLooter~=BROWNIEHELPER_NOMASTERLOOTER then self.mode = BROWNIEHELPER_CLIENT
    else self.mode = BROWNIEHELPER_QUIESCENT
    end
    
    --DEBUG:  pp("mode "..tostring(self.mode))
    --Fire off any events here which should happen when we change modes
    if self.mode~=oldmode or (self.mode~=BROWNIEHELPER_MASTERLOOTER and forceUpdate) then
        --DEBUG:  pp("mode change registered")
        self:TriggerEvent("BrownieHelper_ModeChanged", self.mode)
    end
    
    --Save the ml for the next time we come arround
    self.masterLooterOnLastUpdate = newMasterLooter
    
end

function BrownieHelper:getMode()
    return (self.mode or 0)
end

function BrownieHelper:getMasterLooter()
    --if no roster is supplied we use self.roster; returns name, roster_entry
    
    return (self.currentMasterLooter or BROWNIEHELPER_NOMASTERLOOTER)
    
end

function BrownieHelper:playerIsMasterLooter()
    return UnitName("player")==self:getMasterLooter()
end

function BrownieHelper:playerIsAuditor()
    --See if we can ask the master looter if we're an auditor
    return self:checkPermission("auditors", UnitName("player"))
    
end


--[[
Methods that abstract database access

]]
function BrownieHelper:getDb()
    return self.db.realm
end

function BrownieHelper:getRunDb()
    return self:getDb().runDb
end

function BrownieHelper:getPlayerDb()
    return self:getDb().playerDb
end

function BrownieHelper:getPointsDb()
    if self:getMode()>=BROWNIEHELPER_MASTERLOOTER then
        return self:getDb().pointsDb
    else
        --This attribute will get filled in when the server responds
        return (self.foreignPointsDb or {})
    end
end

function BrownieHelper:getEventsDb()
    if self:getMode()>=BROWNIEHELPER_MASTERLOOTER then
        local run, runDb = self:getCurrentRun()
        
        if runDb~=nil then
            if runDb["events"]==nil then runDb["events"]={} end
            return runDb["events"]
        else return {}
        end
        
    else
        return (self.foreignEventsDb or {})
    end
end

function BrownieHelper:getLootTypesMap()
    return (self:getDb().LootTypes or {})
end

function BrownieHelper:getPermissions()
    return self:getDb().Permissions
end

function BrownieHelper:getPermission(permission_name)
    return self:getPermissions()[permission_name]
end

function BrownieHelper:checkPermission(permissionName, characterName)
    --Alt-safe checking of a permission for a character; default is always to deny.
    
    --translate the local character name to a real player name (in case an auditor is on an alt)
    local playername = self:getPlayerName(characterName)
    
    --if we're the ML we can answer this for ourselves
    if self:playerIsMasterLooter() then
        local permissionDb = self:getPermission(permissionName)
        return has(permissionDb, playername)
        
    --See if we have foreign permissions about us available
    else
        if self.foreignPermissions and characterName==UnitName("player") then
            return has(self.foreignPermissions, permissionName)
        else
            --if we just don't know, return false
            return false
        end
    end
    
end

function BrownieHelper:getRewardEvents()
    return (self:getDb().Events[self:getCurrentPointPool()] or {})
end

--[[
Method that track application state and events

]]
function BrownieHelper:getCurrentRun()
    --returns run_name, runDb_entry
    if self:getMode()>=BROWNIEHELPER_MASTERLOOTER then
        return self.currentrun, self:getRunDb()[self.currentrun]
    else
        return (self.foreignRunName or "waiting for update"), (self.foreignRunInfo or {})
    end
end

function BrownieHelper:setCurrentRun(runName)
    self.currentrun = runName
    
    self:TriggerEvent("BrownieHelper_PointsChanged")
    self:TriggerEvent("BrownieHelper_EventsChanged")
    self:checkForPlayersWithoutPoints()
    
end

function BrownieHelper:getCurrentPointPool()
    --returns poolName, pointsDb_entry
    if self:getMode()>=BROWNIEHELPER_MASTERLOOTER then
        
        local current_run, run_db = self:getCurrentRun()
        
        if current_run~=nil then return run_db.point_pool, self:getPointsDb()[run_db.point_pool]
        else return "none selected", {}
        end
        
    else
        --These attributes will get filled in when the server responds
        return (self.foreignPointPool or "waiting for update").." (from "..(self:getMasterLooter() or "no one")..")", (self.foreignPointDb or {})
        
    end    
end

function BrownieHelper:getPlayerName(characterName)
    --characterName must be in the group (on the roster)
    return self:getRoster()[characterName].playername
end

function BrownieHelper:getCharacterName(playerName)
    --if player is present but on an alt, returns the current character name
    local roster = self:getRoster()
    if has(roster, playerName) then return playerName
    else
        for characterName, rosterInfo in pairs(roster) do
            if rosterInfo.playername == playerName then return characterName end
        end
    end
end

function BrownieHelper:checkAliasesFor(characterName)
    --if the character name is an alias on a player in our database, then we return the player name
    for playerName, playerInfo in pairs(self:getPlayerDb()) do
        if has((playerInfo.aliases or {}), characterName) then return playerName end
    end
    
    --If we're still here just return characterName
    return characterName
end

function BrownieHelper:checkForPlayersWithoutPoints(characterName)
    --if characterName ~= nil then we check everyone
    if self:getMode()>=BROWNIEHELPER_MASTERLOOTER then
    
        local pointPool, pointsDb = self:getCurrentPointPool()
        local source = self:getRoster()
        
        if characterName~=nil then source = {characterName=source[characterName]} end
        
        for characterName, characterInfo in pairs(source) do
            if pointsDb[characterInfo.playername] == nil then
                pointsDb[characterInfo.playername] = "0"
                self:TriggerEvent("BrownieHelper_PointsChanged")
            end
        end
        
    end
    
end

function BrownieHelper:addEvent(event_name, args)
    --[[
    args are key:value pairs where values that are themselves lists should be
    comma (", ") separated for the helper script to parse.  The description
    field will never be parsed as a list.
    
    args.title can suggest a title for the event, otherwise one is calculated
    args.gametime is filled in if not present
    args.players is filled in with players present when the even was recorded if
    its nil and there is no player field defined
    
    if args has .character but not .player we will fill it in from the roster;
    if they are the same, we will delete .character
    ]]
    
    if self:getCurrentRun() and self:getMode()>=BROWNIEHELPER_MASTERLOOTER then
        
        --if we weren't passed args, init a new one
        if args==nil then args={} end
        
        --setup standard things we add and do to make the event pretty and sleek
        if args.event==nil then args.event = event_name end
        if args.gametime==nil then
            args.gametime = getFormattedGameTime() 
        end
        
        if args.character~=nil and args.player==nil then args.player=self:getRoster()[args.character] end
        if args.character==args.player then args.character = nil end
        
        if args.players==nil and args.player==nil then
            local players = {}
            for characterName,characterInfo in pairs(self:getRoster()) do
                table.insert(players, characterInfo.playername)
            end
            table.sort(players)
            args.players = valueJoin(players,", ")
        end
        
        if args.uploaded==nil then args.uploaded = false end
        
        --figure out the event title (entry string in the event database hash)
        local title = ""
        if args.title ~= nil then
            title = args.title
            args.title = nil
        else
            title = args.gametime.." - "..args.event
            if args.player ~= nil then title = title..": "..args.player end
        end
        
        --If there is a reward associated with this event
        local reward = self:getEventReward(event_name)
        reward = tonumber(reward)
        
        if reward and reward~=0 then
            --credit each player on the raid with the reward
            local pointPool, pointDb = self:getCurrentPointPool()
            for characterName, characterInfo in pairs(self:getRoster()) do
                local oldpoints = pointDb[characterInfo.playername]
                pointDb[characterInfo.playername] = (oldpoints or 0) + reward
                self:sendWhisperMessage(characterName, "You have been credited "..tostring(reward).." points, for a new total of "..tostring(pointDb[characterInfo.playername])..".")
            end
            self:TriggerEvent("BrownieHelper_PointsChanged")
            args.reward = tostring(reward)
        end
        
        --Checking with our event checking tags
        if args.rewardevent==nil then
            if event_name=="Raid start time" or event_name=="Time adjustment" then args.rewardevent=true end
        end
        
        if args.rewardevent then
            local runName, runInfo = self:getCurrentRun()
            runInfo.lastRewardEvent = getGameTimeMinutes()
            if event_name == "Raid start time" then
                runInfo.startRecorded = true
            end
        end
        
        --Actually add the event
        self:getEventsDb()[title] = args
        
        --Send out word
        self:TriggerEvent("BrownieHelper_EventsChanged")
        
    end
    
end

function BrownieHelper:deleteEvent(event_name)

    if self:getCurrentRun() and self:getMode()>=BROWNIEHELPER_MASTERLOOTER then
        
        --If there is a reward associated with this event
        local eventInfo = self:getEventsDb()[event_name]
        local reward = self:getEventReward(eventInfo.event)
        reward = tonumber(reward)
        
        if reward and reward~=0 then
            local pointPool, pointDb = self:getCurrentPointPool()
            
            yesnoDialog(
                "There was a reward of "..tostring(reward).." points associated with the event you just deleted.  Do you want to subtract these points from the "..pointPool.." pool for all of the players associated with the event?",
                function(response)
                    if response then
                        
                        for i, characterName in pairs(valueSplit(eventInfo.players, ", ")) do
                            local playerName = self:getPlayerName(characterName)
                            local oldpoints = pointDb[playerName]
                            pointDb[playerName] = (oldpoints or 0) - reward
                        end
                        
                        self:TriggerEvent("BrownieHelper_PointsChanged")
                        
                    end
                end   
            )
            
        end
        
        --Now actually delete the event
        self:getEventsDb()[event_name] = nil
        self:TriggerEvent("BrownieHelper_EventsChanged")
        
    end
    
end

function BrownieHelper:getEventReward(event_name)
    return self:getDb().EventRewards[event_name]
end

function BrownieHelper:checkForEvents()
    
    --Only if we're the ML
    if self:getMode()>=BROWNIEHELPER_MASTERLOOTER then
        
        local runName, runInfo = self:getCurrentRun()
            
        if runInfo then
            local options = self:getDb().Options
            local events = self:getDb().Events[runInfo.point_pool]
            
            --If events are setup for this pool
            if events then
                --If we're auto-firing raid start events
                if options.AutoRecordRaidStart and not runInfo.startRecorded and has(events, 'Raid start time') then
                    if minutesFromTime(runInfo.raidStartTime)<=getGameTimeMinutes() then
                        self:addEvent("Raid start time",{description="Automatically generated raid start time event",rewardevent=true})
                    end
                end
                
                --Only fire time adjustments after the raid has started
                if options.AutoRecordTimeAdjustment and runInfo.startRecorded and has(events, 'Time adjustment') then
                    if getGameTimeMinutes() - runInfo.lastRewardEvent >= tonumber(options.TimeAdjustmentPeriod) then
                        self:addEvent("Time adjustment", {description="Automatically generated time adjustment",rewardevent=true})
                    end
                end
            end
            
        end
        
        --Check back in another minute
        self:ScheduleEvent(function() self:checkForEvents() end , 5) --60,)
        
    end

end

function BrownieHelper:checkForBossEvent(timestamp, event, srcGUID, srcName, srcFlags, dstGUID, dstName, dstFlags)
    if event=="PARTY_KILL" and self:getMode()==BROWNIEHELPER_MASTERLOOTER then
        local options = self:getDb().Options
        local events = self:getRewardEvents()
        if options.AutoRecordBossKills then
            if has(events, dstName) then
                self:addEvent(dstName, {description="Automatically generated boss kill event",rewardevent=true})
            end
        end
    end
end

--[[
State setups that interact with inter-addon communication protocols

]]
function BrownieHelper:OnServerMode()

    --Unregister for any commands we may have been clients for
    if type(self.masterLooterOnLastUpdate)== "string" then
        self:unregisterForCommand(self.masterLooterOnLastUpdate, "GetRunningAuctionUpdates")
        self:unregisterForCommand(self.masterLooterOnLastUpdate, "GetBidUpdates")
        self:unregisterForCommand(self.masterLooterOnLastUpdate, "GetWinnerUpdates")
        self:unregisterForCommand(self.masterLooterOnLastUpdate, "GetLootUpdates")
        self:unregisterForCommand(self.masterLooterOnLastUpdate, "GetBiddingOpen")
    end
    
    --In case we were an auditor before, clear all bids
    self:clearBidding()
    
    --Fire all of the dispatching commands that we send, to update clients as they come in
    self:sendPointUpdates()
    self:sendEventUpdates()
    self:sendBidUpdates()
    
    --Initiate our event checking loop
    self:checkForEvents()

    --Announce to the current group channel if there is one that they should update thier roster (incease we just reloaded our ui)
    self:sendGroupCommand("requestUpdateRoster",{})
    
end

function BrownieHelper:OnAuditorMode()

    local masterLooter = self:getMasterLooter()

    --Do all of the client mode stuff
    self:OnClientMode()
    
    --Register for protected callbacks
    self:registerForCommand(masterLooter, "GetRunningAuctionUpdates")
    self:registerForCommand(masterLooter, "GetBidUpdates")
    self:registerForCommand(masterLooter, "GetWinnerUpdates")

end

function BrownieHelper:OnClientMode()

    local masterLooter = self:getMasterLooter()

    --Do all of the quiescent stuff
    self:OnQuiescentMode()

    --Whenever we're in client mode, register for loot item updates
    self:registerForCommand(masterLooter, "GetLootUpdates")
    self:registerForCommand(masterLooter, "GetBiddingOpen")
    
    --Get our permissions
    self:callGetPermissions(masterLooter)
    self:registerForCommand(masterLooter, "GetPermissionsChanged")
    
end

function BrownieHelper:OnQuiescentMode()

    --In case we were the ML before, clear all bids
    self:clearBidding()

    --In case we were just the server, clear out our command request queue
    self:unregisterClients()
    
    --Clear any prior reciept of foreign point data
    self.foreignPointPool = "not set"
    self.foreignPointsDb = {}
    
    --Clear any reciept of foreign event data
    self.foreignEventDb = {}
    
    --Clear any previous permission
    self.foreignPermissions = {}
    
end

--[[
Communication request and response handlers

]]
--HasBrownieHelper command
function BrownieHelper:requestHasBrownieHelper(sender, args, responseCall)
    --Handles an incomming request asking if we have browniehelper; which we obviously do!
        
    --If its the masterLooter who's asking, check to make sure they haven't just reloaded
    if sender==self:getMasterLooter() and self:getMode()<=BROWNIEHELPER_AUDITOR then
        self:TriggerEvent("BrownieHelper_ModeChanged", self:getMode())
    end
    
    --It doesn't matter who's asking, just respond that we do, args is a hash with a version tag
    responseCall({
        brownieHelperVersion=BROWNIEHELPER_VERSION,
        lastExternalUpdate=self:getDb().lastExternalUpdate,
        lastInternalUpdate=self:getDb().lastInternalUpdate,
        })
    
end
function BrownieHelper:respondHasBrownieHelper(sender, args)
    --Record that the sender has brownie helper (note we could reject clients in the future if they have an incompatible version tag in args)
    
    if not self.brownieHelperRoster then self.brownieHelperRoster = {} end
    
    --If this is a player we are tracking as in the group
    self.brownieHelperRoster[sender] = {}
    for k,v in pairs(args) do self.brownieHelperRoster[sender][k] = (v or "") end
    
    --Update the sources view
    if ViewDatabaseSourcesFrame then ViewDatabaseSourcesFrame:updateView() end
    
end

--GetPermissions command
function BrownieHelper:requestGetPermissions(sender, args, responseCall)
    --called when a request for the permissions hash is recieved
    
    --Build a hash the permissions the sender has
    local permissions = {}
    for permissionName, db in pairs(self:getPermissions()) do
        if self:checkPermission(permissionName, sender) then table.insert(permissions, permissionName) end
    end
    
    --If we're the master looter respond with our permissions hash
    if self:playerIsMasterLooter() then responseCall({permissions=permissions}) end
    
end
function BrownieHelper:respondGetPermissions(sender, args)
    --called when the master looter responds with their permissions hash
    
    --If the sender is in fact the master looter, then save their permissions hash
    if sender==self:getMasterLooter() then
        --save the permissions hash
        self.foreignPermissions = args.permissions
        --update our mode (incase the permissions change things...)
        self:updateMode()
    end
    
end

--GetPointUpdates dispatching command
function BrownieHelper:sendPointUpdates()
    if self:getMode()>=BROWNIEHELPER_MASTERLOOTER then
        local pointPool, pointsDb = self:getCurrentPointPool()
        self:dispatchGetPointUpdates({pointsDb=pointsDb,point_pool=pointPool})
    end
end
function BrownieHelper:respondGetPointUpdates(sender, args)
    --[[
    incomming args = {
        pointsDb = {playerName:"points",},
        point_pool = "pool name",
    }
    ]]
    
    --Save the values
    self.foreignPointPool = args.point_pool
    self.foreignPointDb = args.pointsDb
    
    --Trigger a point pool update
    self:TriggerEvent("BrownieHelper_PointsChanged")
end

--GetEventUpdates dispatching command
function BrownieHelper:sendEventUpdates()
    --if we're the master looter
    if self:getMode() >= BROWNIEHELPER_MASTERLOOTER then
        local runName, runInfo = self:getCurrentRun()
        self:dispatchGetEventUpdates({runName=runName, runInfo=runInfo, eventsDb=self:getEventsDb()})
    end
end
function BrownieHelper:respondGetEventUpdates(sender, args)
    --[[
    incomming args = {eventsDb=self:getEventsDb(),runName=runName,runInfo=runInfo}
    ]]
    
    --Save the values
    self.foreignEventsDb = args.eventsDb
    self.foreignRunName = args.runName
    self.foreignRunInfo = args.runInfo
    
    --Trigger a point pool update
    self:TriggerEvent("BrownieHelper_EventsChanged")
end

--GetLootUpdates dispatching command
function BrownieHelper:clearLootUpdates()
    --For players with the addon
    self:dispatchGetLootUpdates({})
end
function BrownieHelper:sendLootUpdates()
    if self:getMode() >= BROWNIEHELPER_MASTERLOOTER then
        local loot = self:getLoot()
        
        if loot then
            --For players with the addon
            self:dispatchGetLootUpdates({loot=self:cleanLoot(loot)})
            
            --For everyone else
            local lines = {"The following items are going to be up for auction:"}
            for k,itemInfo in pairs(loot) do
                table.insert(lines, itemInfo.link)
            end
            self:sendGroupMessage(lines)
        end
        
    end
end
function BrownieHelper:respondGetLootUpdates(sender, args)
    
    --If theres loot in the update
    if args.loot then
        --Save the values
        self.foreignLoot = args.loot
        
        --Trigger the loot update event
        self:TriggerEvent("BrownieHelper_LootChanged")
        
        --Show the items window, if the user wants to see it
        if not ViewItemsFrame:IsShown() and self:getDb().Options['ShowItemWindow'] then ViewItemsFrame:Show() end
    else
        if ViewItemsFrame:IsShown() then ViewItemsFrame:Hide() end
    end
    
end

--GetPermissionsChanged dispatching command
function BrownieHelper:sendGetPermissionsChanged()
    if self:getMode()>=BROWNIEHELPER_MASTERLOOTER then
        self:dispatchGetPermissionsChanged({})
    end
end

function BrownieHelper:respondGetPermissionsChanged(sender, args)

    --Get our permissions anew
    local masterLooter = self:getMasterLooter()
    if masterLooter then self:callGetPermissions(masterLooter) end
    
end

--GetRunningAuctionUpdates dispatching command
function BrownieHelper:sendRunningAuctionUpdates(state, itemInfo, bidSetup, bids)

    if self:getMode()>=BROWNIEHELPER_MASTERLOOTER then
        --Dispatched cleaned data structures
        if itemInfo and bidSetup then self:dispatchGetRunningAuctionUpdates({state=state, itemInfo=self:cleanItemInfo(itemInfo), bidSetup=self:cleanBidSetup(bidSetup)}) end
    end

end
function BrownieHelper:respondGetRunningAuctionUpdates(sender, args)
    
    --Trigger the update event
    self:TriggerEvent("BrownieHelper_AuctionStateChanged", args.state, args.itemInfo, self:uncleanBidSetup(args.bidSetup))

end

--GetBidUpates dispatching command
function BrownieHelper:sendBidUpdates()
    --We need to clean up bids for serialization
    if self:getMode()>=BROWNIEHELPER_MASTERLOOTER then
        if self.bids then
            self:dispatchGetBidUpdates({bids=self:cleanBids(self.bids)})
        end
    end
end
function BrownieHelper:respondGetBidUpdates(sender, args)
    self.bids = args.bids
    self:TriggerEvent("BrownieHelper_BidsChanged", args.bids)
end

--GetWinnerUpdates
function BrownieHelper:sendWinnerUpdates()
    if self:getMode()>=BROWNIEHELPER_MASTERLOOTER then
        self:dispatchGetWinnerUpdates({winningBid=(self.winningBid and self:cleanBid(self.winningBid) or nil)})
    end
end
function BrownieHelper:respondGetWinnerUpdates(sender, args)
    self.winningBid = args.winningBid
    self:TriggerEvent("BrownieHelper_WinnerChanged")
end

--BiddingOpen dispatching command
function BrownieHelper:sendBiddingOpen(bidSetup)
    if self:getMode()>=BROWNIEHELPER_MASTERLOOTER then
        if bidSetup then self:dispatchGetBiddingOpen({biddingOpen=true})
        else self:dispatchGetBiddingOpen({biddingOpen=false})
        end
    end
end
function BrownieHelper:respondGetBiddingOpen(sender, args)
    --Request details about the auction tailored to us
    if args.biddingOpen==true then self:callGetAuctionDetails(sender, args)
    else self:TriggerEvent("BrownieHelper_BiddingOpened", false)
    end
end

--GetAuctionDetails  command
function BrownieHelper:requestGetAuctionDetails(sender, args, responseCall)
    --Here's where we custom tailor a bid setup for a particular player
    responseCall({playerBidSetup=self:getPlayerBidSetup(sender)})
end
function BrownieHelper:respondGetAuctionDetails(sender, args)
    if sender==self:getMasterLooter() then self:TriggerEvent("BrownieHelper_BiddingOpened", args.playerBidSetup) end
end

--SubmitBid command
function BrownieHelper:requestSubmitBid(sender, args, responseCall)
    --args = {bid=int}
    responseCall({result=self:addBid(sender, args.bid)})
end
function BrownieHelper:respondSubmitBid(sender, args)
    --we get a true | false result back in args
    if args.result==true then AddBidFrame:Hide() end
end

--Force roster and mode update group command
function BrownieHelper:requestUpdateRoster(sender, args)

    self:processRosterUpdate()
    self:updateMode(true)

end

--Functions to clean up data structures for serialization
function BrownieHelper:cleanBid(bid)
    return {
        charactername=bid.charactername,
        playername = bid.playername,
        bid = bid.bid,
        roll = bid.roll,
        playerpoints = getPlayerPoints(bid.charactername, self.bidSetup),
    }
end

function BrownieHelper:cleanBids(bids)
    local sentBids = {}
    for n,bid in pairs(bids) do
        sentBids[n] = self:cleanBid(bid)
    end
    return sentBids
end

function BrownieHelper:cleanItemInfo(itemInfo)
    if not itemInfo then return {} end

    local sentItemInfo = {
        name = itemInfo.name,
        link = itemInfo.link,
        biddingsystems = {},
    }

    for i,biddingsystem in pairs((itemInfo.biddingsystems or {})) do
        sentItemInfo.biddingsystems[i] = {name=biddingsystem.name}
    end
    
    return sentItemInfo
    
end

function BrownieHelper:cleanLoot(loot)
    local sentLoot = {}
    for i,itemInfo in pairs(loot) do
        sentLoot[i] = self:cleanItemInfo(itemInfo)
    end
    return sentLoot
end

function BrownieHelper:cleanBidSetup(bidSetup)
    return {
        
        itemInfo = self:cleanItemInfo(bidSetup.itemInfo),
        nextPhase = (bidSetup.nextPhase and true or false),
        nextItem = (bidSetup.nextItem and true or false),
        itemname = bidSetup.itemname,
        itemlink = bidSetup.itemlink,
        minbid = bidSetup.minbid,
        phase = bidSetup.phase,
        
        --We'll translate bidding system to a string, so it can be retrieved on the client from BiddingSystems (hopefully!)
        biddingsystem = (bidSetup.biddingsystem and bidSetup.biddingsystem.name or ""),
    }
end

function BrownieHelper:uncleanBidSetup(bidSetup)
    bidSetup.biddingsystem = BiddingSystems[bidSetup.biddingsystem]
    bidSetup.app = self
    return bidSetup
end
